package cn.ycc1.functionlibrary.streams;

/**
 * Adding Intermediate Operations on a Stream
 * @author ycc
 * @date 2025/3/7
 */
public class AddingIntermediate {
    /**
     * Mapping a Stream to Another Stream
     * Mapping a stream consists of transforming its elements using a function. This transformation may change the types of the
     * elements processed by that stream, but you can also transform them without changing their type.
     *
     * You can map a stream to another stream with the map() method, which takes this Function as an argument.
     * Mapping a stream means that all the elements processed by that stream will be transformed using that function.
     *
     * You can run this code in your browser, or copy and paste it in your IDE to run it. You will not see anything,
     * and you may be wondering why.
     *
     * The answer is in fact simple: there is no terminal operation defined on that stream. Your reflex should be to notice
     * that and realize that this code does not do anything. It does not process any data. To answer the question:
     * "What is this code doing?", there is only one valid answer: "Nothing".
     *
     * Let us add a very useful terminal operation, which puts the processed elements in a list: toList(). If you are not
     * sure about what this code really does, do not worry; we will cover that later in this tutorial. The code becomes the
     * following.
     *
     * Running this code prints the following:
     *
     * lengths = [3, 3, 5, 4]
     * You can see that this pattern creates a Stream<Integer>, returned by the map(String::length). You can also make it a
     * specialized IntStream by calling mapToInt() instead of the regular map() call. This mapToInt() method takes a
     * ToIntFuction<T> as an argument. Changing .map(String::length) to .mapToInt(String::length) in the previous example
     * does not create a compiler error. The method reference String::length can be of both types: Function<String, Integer>
     *     and ToIntFunction<String>.
     *
     * Instead of calling toList() we now call summaryStatistics(). This method is very handy and is only available on these
     * specialized streams of primitive types.
     *
     * The result is the following:
     *
     * stats = IntSummaryStatistics{count=4, sum=15, min=3, average=3,750000, max=5}
     * There are three methods to go from Stream to a stream of primitive type: mapToInt(), mapToLong() and mapToDouble().
     */

    /**
     * iltering a Stream
     * Filtering is about discarding some elements processed by a stream with a predicate. This method is available on streams
     * of objects and stream of primitive types.
     *
     * Suppose you need to count the strings of characters of length 3. You can write this code to do that:
     * Running this code produces the following:
     *
     * count = 2
     * Notice that you just used another terminal operation of the Stream API, count(), which just counts the number of
     * processed elements. This method returns a long, so you can count a lot of elements with it. More than you can put in
     * an ArrayList.
     */

    /**
     * Flatmapping a Stream to Handle 1:p Relations
     *
     * Let us see the flatMap operation in an example.
     *
     * Suppose your code is processing a list of countries, and at some point you need to count the population of all the cities.
     * City and Country are two records, and an instance of State has a reference to a list of instances of City.
     *
     * You may write the following code:
     *
     * The result you get is the following.
     *
     * Total population = 24493
     * The inner loop of this code is a form of map-reduce that you can write with the following stream (you can copy this code
     * and paste it in the previous example and run it again):
     *
     * totalPopulation += country.cities().stream().mapToInt(City::population).sum();
     * The connection between the loop on the countries and this stream does not fit well in the map-reduce pattern, and putting
     * a stream in a loop is not a very nice pattern of code.
     *
     * This is precisely the role of the flatmap operator. This operator opens one-to-many relations between objects and create
     * streams on these relations. The flatMap() method takes a special function as an argument that returns a Stream object.
     * The relationship between a given class and another class is defined by this function.
     *
     * In the case of our example, this function is simple because there is a List<City> in the Country class. So you can write
     * it in the following way.
     *
     * Function<Country, Stream<City>> countryToCity = country -> country.cities().stream();
     * This list is not mandatory. Suppose you have a Continent class that holds a Map<String, Country>, where the keys are
     * the country codes of the countries (CAN for Canada, MEX for Mexico, FRA for France, and so on). Suppose that the
     * Continent record has a component countries that is this map.
     *
     * In that case, this function could be written in this way.
     *
     * Function<Continent, Stream<Country>> continentToCountry =
     *     continent -> continent.countries().values().stream();
     * The flatMap() method processes a stream in two streps.
     *
     * The first step consists of the mapping of all the elements of the stream with this function. From a Stream<Country> it
     * creates a Stream<Stream<City>>, because every country is mapped to a stream of cities.
     * The second step consists of flattening the stream of streams that is produced. Instead of having a stream of streams of
     * cities (one stream for each country), you end up with a single stream, with all the cities of all the countries in it.
     */

    /**
     * Using Flatmap and MapMulti to Validate Elements Transformation
     * The flatMap operation can be used to validate the transformation of the elements of your stream.
     *
     * Suppose you have a stream of strings of characters, that represent integers. You need to convert them to integers using
     * Integer.parseInt(). Unfortunately some of these strings are corrupted: maybe some are empty, null, or have extra blank
     * characters at the end of them. All these will make the parsing fail with a NumberFormatException. Of course, you can
     * try to filter this stream to remove the buggy strings with predicates, but the safest way to do that is to use a
     * try-catch pattern.
     *
     * Trying to use a filter is not the right way to go. The predicate you are going to write will look like the following.
     *
     * Predicate<String> isANumber = s -> {
     *     try {
     *         int i = Integer.parseInt(s);
     *         return true;
     *     } catch (NumberFormatException e) {
     *         return false;
     *     }
     * };
     * This first flaw is that you need to actually do the conversion to see if it is working or not. Then you will have to
     * do it again in your mapping function, which will be executed next: don't do that!
     *
     * The second flaw is that it is never a good idea to return from a catch block.
     *
     * What you really need to do is to return an integer when you have a proper integer in this string and nothing if it is
     * a corrupted string. This can be done with a flatmapper. In the case your string of characters represents an integer,
     * you return a stream with the result. In the other case, you can return an empty stream.
     */

    /**
     * Removing Duplicates and Sorting a Stream
     * The Stream API has two methods, distinct() and sorted(), that will simply detect and remove duplicates and sort the
     * elements of your stream. The distinct() method uses the hashCode() and equals() methods to spot the duplicates.
     * The sorted() method has an overload that takes a comparator, which will be used to compare and sort the elements
     * of your stream. If you do not provide a comparator, then the Stream API assumes that the elements of your stream are
     * comparable. If they are not, then a ClassCastException is raised.
     *
     * You may remember from the previous part of this tutorial that a stream is supposed to be an empty object that does not
     * store any data. There are several exceptions to this rule, and these two methods belong to them.
     *
     * Indeed, to spot duplicates, the distinct() method needs to store the elements of your stream. It does so in an internal
     * Set. When it processes an element, it adds it to this set. If the addition worked, meaning that the element was not in
     * the set before, it immediately pushes it to the downstream.
     *
     * The same goes for the sorted() method. This method needs to store all of your elements and then to sort them in an
     * internal buffer before sending them to the next step of your processing pipeline.
     *
     * Here are two examples of these methods in action. On this first example, you can see how distinct() is working.
     */

    /**
     * Limiting and Skipping the Elements of a Stream
     * The Stream API gives you two ways of selecting the elements of a stream: based on their index, or with a predicate.
     *
     * The first way is to use the skip() and limit() methods, both take a long as an argument. There is a little trap to avoid
     * when you use these methods. You need to keep in mind that every time an intermediate method is called on stream,
     * a new stream is created. So if you call limit() after skip(), do not forget to count your elements starting on that
     * new stream.
     *
     * Suppose you have a stream of all the integers, starting at 1. You need to select the integers between 3 and 8 on a stream
     * of integers. You may be tempted to call skip(2).limit(8), passing the bound computed on the first stream.
     * Unfortunately this is not how streams work. The second call limit(8) operates on a stream that starts at 3,
     * so it will select the integers until 11, which is not what you need. The correct code is the following.
     */

    /**
     * Concatenating Streams
     * The Stream API offers several patterns to concatenate several streams into one. The most obvious way is to use a factory
     * method defined in the Stream interface: concat().
     *
     * This method takes two streams and produces a stream with the elements produced by the first stream, followed by the
     * elements of the second stream.
     *
     * You may be wondering why this method does not take a vararg to allow for the concatenation of any number of streams.
     *
     * The reason is that using this method is OK as long as you have two streams to join. If you have more than two,
     * then the JavaDoc API documentation advises you to use another pattern, based on the use of flatmap.
     */

    /**
     * Debugging Streams
     * It may sometimes be convenient to examine the elements processed by a stream at run time. The Stream API has a method
     * for that: the peek() method. This method is meant to be used to debug your data processing pipeline.
     * You should not use this method in your production code.
     *
     * You should absolutely refrain from using the peek() method to perform some side effects in your application.
     *
     * This method takes a consumer as an argument that will be invoked by the API on each element of the stream.
     * Let us see this method in action.
     *
     * Let us analyze this output.
     *
     * The first element to be processed is one. You can see that it was filtered out.
     * The second is two. This element passed the filter and then is mapped to upper case. It is then added to the result list.
     * The third one is three, that also passes the filter and is also mapped to upper case before being added to the result
     * list.
     * The fourth and last one is four that is rejected by the filtering step.
     * There is one point that you saw earlier in this tutorial and that appears clearly now: a stream does process all the
     * elements it has to process one by one, from the beginning to the end of the stream. This was mentioned before,
     * and now you can see it in action.
     *
     * You can see that this peek(System.out::println) pattern is very useful to follow the elements processed by your stream
     * one by one, without having to debug your code. Debugging a stream is hard because you need to be careful where you put
     * your breakpoints. Most of the time, putting breakpoints on a stream processing will send you to the implementation of
     * the Stream interface. This is not what you need. Most of the time you need to put these breakpoints in the code of your
     * lambda expressions.
     */


}
